

package com.googlecode.flac4j.format.frame;


import com.googlecode.flac4j.format.FormatHelper;
import com.googlecode.flac4j.format.util.CRC8;

import com.googlecode.jinahya.io.BitInput;
import com.googlecode.jinahya.io.BitOutput;
import com.googlecode.jinahya.util.BitAccessible;

import java.io.IOException;

import java.util.zip.Checksum;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;


/**
 *
 * @author Jin Kwon <jinahya at gmail.com>
 */
@XmlType(propOrder = {"blockingStrategy", "blockSizeHint", "sampleRateHint",
                      "channelAssignment", "sampleSizeHint", "sampleNumber",
                      "frameNumber"})
public class Header implements BitAccessible {


    /** Sync code '11111111111110'. */
    public static final int SYNC_CODE = 16382; // '11 1111 1111 1110'


    public static final int FIXED_BLOCKSIZE_STREAM = 0;


    public static final int VARIABLE_BLOCKSIZE_STREAM = 1;


    @Override
    public void read(final BitInput input) throws IOException {

        assert input.available() == 0;

        final Checksum checksum = new CRC8();
        input.addChecksum(checksum);
        try {
            readContent(input);
            assert input.available() == 0;
        } finally {
            input.removeChecksum(checksum);
        }

        assert checksum.getValue() == input.readUnsignedLong(8);
    }


    protected void readContent(final BitInput input) throws IOException {

        assert input.readUnsignedInt(14) == SYNC_CODE;

        assert input.readUnsignedInt(1) == 0; // reserved

        setBlockingStrategy(input.readUnsignedInt(1));

        blockSizeHint = input.readUnsignedInt(4);

        sampleRateHint = input.readUnsignedInt(4);

        setChannelAssignment(input.readUnsignedInt(4));

        sampleSizeHint = input.readUnsignedInt(3);

        assert input.readUnsignedInt(1) == 0; // reserved

        assert input.available() == 0;

        if (blockingStrategy == VARIABLE_BLOCKSIZE_STREAM) {
            setSampleNumber(FormatHelper.readLongAsUTF8(input));
        } else {
            setFrameNumber(FormatHelper.readIntAsUTF8(input));
        }

        switch (blockSizeHint) {
            case 0x06: // 0110
                blockSize = input.readUnsignedInt(0x08) + 1;
                break;
            case 0x07: // 0111
                blockSize = input.readUnsignedInt(0x10) + 1;
                break;
            default:
                break;
        }

        switch (sampleRateHint) {
            case 0x0C: // 1100
                sampleRate = input.readUnsignedInt(8) * 1000;
                break;
            case 0x0D: // 1101
                sampleRate = input.readUnsignedInt(16);
                break;
            case 0x0E: // 1110
                sampleRate = input.readUnsignedInt(16) * 10;
                break;
            default:
                break;
        }
    }


    @Override
    public void write(final BitOutput output) throws IOException {

        assert output.available() == Byte.SIZE;

        final Checksum checksum = new CRC8();
        output.addChecksum(checksum);
        try {
            writeContent(output);
            assert output.available() == Byte.SIZE;
        } finally {
            output.removeChecksum(checksum);
        }

        output.writeUnsignedLong(8, checksum.getValue());
    }


    /**
     * Writes content exception CRC to given <code>output</code>.
     *
     * @param output output
     * @throws IOException if an I/O error occurs.
     */
    protected void writeContent(final BitOutput output) throws IOException {

        output.writeUnsignedInt(14, SYNC_CODE);

        output.writeUnsignedInt(1, 0); // reserved

        output.writeUnsignedInt(1, blockingStrategy);

        output.writeUnsignedInt(4, blockSizeHint);

        output.writeUnsignedInt(4, sampleRateHint);

        output.writeUnsignedInt(4, channelAssignment);

        output.writeUnsignedInt(3, sampleSizeHint);

        output.writeUnsignedInt(1, 0); // reserved

        if (blockingStrategy == VARIABLE_BLOCKSIZE_STREAM) {
            FormatHelper.writeLongAsUTF8(output, sampleNumber);
        } else {
            FormatHelper.writeIntAsUTF8(output, frameNumber);
        }

        switch (blockSizeHint) {
            case 0x06: // 0110
                output.writeUnsignedInt(0x08, blockSize - 1);
                break;
            case 0x07: // 0111
                output.writeUnsignedInt(0x10, blockSize - 1);
            default:
                break;
        }

        switch (sampleRateHint) {
            case 0x0C: // 1100
                output.writeUnsignedInt(8, sampleRate / 1000);
                break;
            case 0x0D: // 1101
                output.writeUnsignedInt(16, sampleRate);
                break;
            case 0x0E: // 1110
                output.writeUnsignedInt(16, sampleRate / 10);
                break;
            default:
                break;
        }
    }


    // ------------------------------------------------------------ parent frame
    /**
     * Returns the parent frame of this header.
     *
     * @return parent frame
     */
    public final Frame getFrame() {
        return frame;
    }


    /**
     * Sets the parent frame of this header.
     *
     * @param frame frame
     */
    public final void setFrame(final Frame frame) {

        if (frame == null) {
            throw new NullPointerException("null frame");
        }

        this.frame = frame;
    }


    // ----------------------------------------------------------- blockStrategy
    /**
     * Returns blockingStrategy.
     *
     * @return blockingStrategy.
     */
    public int getBlockingStrategy() {
        return blockingStrategy;
    }


    /**
     * Sets blockingStratege.
     *
     * @param blockingStrategy blockingStrategy.
     */
    public void setBlockingStrategy(final int blockingStrategy) {

        if (blockingStrategy >> 1 != 0) {
            throw new NullPointerException(
                "blockingStrategy(" + blockingStrategy + ") >> 1 != 0");
        }

        this.blockingStrategy = blockingStrategy;
    }


    // --------------------------------------------------------------- blockSize
    /**
     * Returns blockSizeHint.
     *
     * @return blockSizeHint
     */
    public int getBlockSizeHint() {
        return blockSizeHint;
    }


    /**
     * Sets blockSizeHint.
     *
     * @param blockSizeHint blockSizeHint
     */
    public void setBlockSizeHint(final int blockSizeHint) {

        if (blockSizeHint >> 4 != 0) {
            throw new IllegalArgumentException(
                "blockSizeHint(" + blockSizeHint + ") >> 4 != 0");
        }

        switch (blockSizeHint) {
            case 0x00:
                break;
            case 0x01:
                blockSize = 192;
                break;
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
                blockSize = 576 * (int) Math.pow(2, blockSizeHint - 2);
                break;
            case 0x06:
                // get 8 bit (blocksize-1) from end of header
                break;
            case 0x07:
                // get 16 bit (blocksize-1) from end of header
                break;
            case 0x08:
            case 0x09:
            case 0x0A:
            case 0x0B:
            case 0x0C:
            case 0x0D:
            case 0x0E:
            case 0x0F:
                blockSize = 256 * (int) Math.pow(2, blockSizeHint - 8);
                break;
            default:
                throw new IllegalArgumentException(
                    "illegal blockSize: " + blockSizeHint);
        }

        this.blockSizeHint = blockSizeHint;
    }


    /**
     * Returns blockSizeValue.
     *
     * @return blockSizeValue
     */
    public int getBlockSize() {
        return blockSize;
    }


    /**
     * Sets blockSizeValue.
     *
     * @param blockSize blockSizeValue
     */
    public void setBlockSize(final int blockSize) {

        if (blockSize <= 0) {
            throw new IllegalArgumentException(
                "blockSize(" + blockSize + ") <= 0");
        }

        if (blockSize > 65536) {
            throw new IllegalArgumentException(
                "blockSize(" + blockSize + ") > 65536");
        }

        switch (blockSize) {
            case 0x00C0: // 192
                blockSizeHint = 0x01; // 0001
                break;
            case 0x0240: // 576
            case 0x0480: // 1152
            case 0x0900: // 2304
            case 0x1200: // 4608
                blockSizeHint = FormatHelper.log2(blockSize / 576) + 2;
                break;
            case 0x0100: // 256
            case 0x0200: // 512
            case 0x0400: // 1024
            case 0x0800: // 2048
            case 0x1000: // 4096
            case 0x2000: // 8192
            case 0x4000: // 16384
            case 0x8000: // 32768
                blockSizeHint = FormatHelper.log2(blockSize / 256) + 8;
                break;
            default:
                if (blockSize <= 256) {
                    blockSizeHint = 0x06; // 0110
                } else {
                    blockSizeHint = 0x07; // 0111
                }
                break;
        }

        this.blockSize = blockSize;
    }


    // -------------------------------------------------------------- sampleRate
    /**
     * Returns sampleRateHint.
     *
     * @return sampleRateHint
     */
    public int getSampleRateHint() {
        return sampleRateHint;
    }


    /**
     * Sets sampleRateHint.
     *
     * @param sampleRateHint sampleRateHint
     */
    public void setSampleRateHint(final int sampleRateHint) {

        if (sampleRateHint >> 4 != 0) {
            throw new IllegalArgumentException(
                "sampleRateHint(" + sampleRateHint + ") >> 4 != 0");
        }

        switch (sampleRateHint) {
            case 0x00: // 0000
                sampleRate =
                    frame.getStream().getStreamInfoBlockData().getSampleRate();
                break;
            case 0x01: // 0001
                sampleRate = 88200; // 88.2kHz
                break;
            case 0x02: // 0010
                sampleRate = 176400; // 176.4kHz
                break;
            case 0x03: // 0011
                sampleRate = 192000; // 192kHz
                break;
            case 0x04: // 0100
                sampleRate = 8000; // 8kHz
                break;
            case 0x05: // 0101
                sampleRate = 16000; // 16kHz
                break;
            case 0x06: // 0110
                sampleRate = 22050; // 22.05kHz
                break;
            case 0x07: // 0111
                sampleRate = 24000; // 24kHz
                break;
            case 0x08: // 1000
                sampleRate = 32000; // 32kHz
                break;
            case 0x09: // 1001
                sampleRate = 44100; // 44.1kHz
                break;
            case 0x0A: // 1010
                sampleRate = 48000; // 48kHz
                break;
            case 0x0B: // 1011
                sampleRate = 96000; // 96kHz
                break;
            case 0x0C: // 1100
                // get 8 bit sample rate (in kHz) from end of header
                break;
            case 0x0D: // 1101
                // get 16 bit sample rate (in Hz) from end of header
                break;
            case 0x0E: // 1110
                // get 16 bit sample rate (in tens of Hz) from end of header
                break;
            case 0x0F: // 1111
                throw new IllegalArgumentException(
                    "invalid sampleRateFlag(" + sampleRateHint + ")");
            default:
                break;
        }

        this.sampleRateHint = sampleRateHint;

    }


    // -------------------------------------------------------------- sampleRate
    /**
     * Returns sampleRate.
     *
     * @return sampleRate
     */
    public int getSampleRate() {

        return sampleRate;
    }


    /**
     * Sets sampleRate.
     *
     * @param sampleRate sampleRate
     */
    public void setSampleRate(final int sampleRate) {

        if (sampleRate < 0) {
            throw new IllegalArgumentException(
                "sampleRate(" + sampleRate + ") < 0");
        }

        if (sampleRate > 655350) {
            throw new IllegalArgumentException(
                "sampleRate(" + sampleRate + ") > 655350");
        }

        switch (sampleRate) {
            case 88200: // 88.2kHz
                sampleRateHint = 0x01; // 0001
                break;
            case 176400: // 176.4 kHz
                sampleRateHint = 0x02; // 0010
                break;
            case 192000: // 192kHz
                sampleRateHint = 0x03; // 0011
                break;
            case 8000: // 8kHz
                sampleRateHint = 0x04; // 0100
                break;
            case 16000: // 16kHz
                sampleRateHint = 0x05; // 0101
                break;
            case 22050: // 22.05kHz
                sampleRateHint = 0x06; // 0110
                break;
            case 24000: // 24kHz
                sampleRateHint = 0x07; // 0111
                break;
            case 32000: // 32kHz
                sampleRateHint = 0x08; // 1000
                break;
            case 44100: // 44.1kHz
                sampleRateHint = 0x09; // 1001
                break;
            case 48000: // 48kHz
                sampleRateHint = 0x0A; // 1010
                break;
            case 96000: // 96kHz
                sampleRateHint = 0x0B; // 1011
                break;
            default:
                if (sampleRate <= 65535) {
                    sampleRateHint = 0x0D; // 1101
                } else if (sampleRate <= 255000) {
                    sampleRateHint = 0x0C; // 1100
                } else { // sampleRateValue <= 655350
                    sampleRateHint = 0x0E; // 1110
                }
                break;
        }

        this.sampleRate = sampleRate;
    }


    // ------------------------------------------------------- channelAssignment
    /**
     * Returns channelAssignment.
     *
     * @return channelAssignment
     */
    public int getChannelAssignment() {

        return channelAssignment;
    }


    /**
     * Sets channelAssignment.
     *
     * @param channelAssignment channelAssignment
     */
    public void setChannelAssignment(final int channelAssignment) {

        if (channelAssignment >> 4 != 0) {
            throw new IllegalArgumentException(
                "channelAssignment(" + channelAssignment + ") >> 4 != 0");
        }

        this.channelAssignment = channelAssignment;
    }


    // -------------------------------------------------------------- sampleSize
    /**
     * Returns sampleSizeHint.
     *
     * @return sampleSizeHint.
     */
    public int getSampleSizeHint() {
        return sampleSizeHint;
    }


    /**
     * Sets sampleSizeHint.
     *
     * @param sampleSizeHint sampleSizeHint
     */
    public void setSampleSizeHint(final int sampleSizeHint) {

        if (sampleSizeHint >> 3 != 0) {
            throw new IllegalArgumentException(
                "sampleSizeHint(" + sampleSizeHint + ") >> 3 != 0");
        }

        switch (sampleSizeHint) {
            case 0x00: // 000 get from STREAMINFO metadata block 
                sampleSize =
                    frame.getStream().getStreamInfoBlockData().getSampleSize();
                break;
            case 0x01: // 001
                sampleSize = 8;
                break;
            case 0x02: // 010
                sampleSize = 12;
                break;
            case 0x03: // 011 reserved
                break;
            case 0x04:
                sampleSize = 16;
                break;
            case 0x05: // 101
                sampleSize = 20;
            case 0x06: // 110
                sampleSize = 24;
                break;
            case 0x07: // 111 reserved
                break;
            default:
                throw new IllegalArgumentException(
                    "illegal sampleSizeHint: " + sampleSizeHint);
        }

        this.sampleSizeHint = sampleSizeHint;
    }


    /**
     * Returns sampleSizeValue.
     *
     * @return sampleSizeValue
     */
    public int getSampleSize() {
        return sampleSize;
    }


    /**
     * Sets sampleSize.
     *
     * @param sampleSize sampleSize
     */
    public void setSampleSize(final int sampleSize) {

        switch (sampleSize) {
            case 0x08: // 8
                sampleSizeHint = 0x01; // 001
                break;
            case 0x0C: // 12
                sampleSizeHint = 0x02; // 010
                break;
            case 0x10: // 16
                sampleSizeHint = 0x04; // 100
                break;
            case 0x14: // 20
                sampleSizeHint = 0x05; // 101
                break;
            case 0x18: // 24
                sampleSizeHint = 0x06; // 110
                break;
            default:
                throw new IllegalArgumentException(
                    "illegal sampleSize: " + sampleSize);
        }

        this.sampleSize = sampleSize;
    }


    // ------------------------------------------------------------ sampleNumber
    public long getSampleNumber() {
        return sampleNumber;
    }


    public void setSampleNumber(final long sampleNumber) {

        if (sampleNumber < 0L) {
            throw new IllegalArgumentException(
                "sampleNumber(" + sampleNumber + ") < 0L");
        }

        if (sampleNumber != 0) {
            blockingStrategy = VARIABLE_BLOCKSIZE_STREAM;
            frameNumber = 0;
        }

        this.sampleNumber = sampleNumber;
    }


    // ------------------------------------------------------------- frameNumber
    public int getFrameNumber() {
        return frameNumber;
    }


    public void setFrameNumber(final int frameNumber) {

        if (frameNumber < 0L) {
            throw new IllegalArgumentException(
                "frameNumber(" + frameNumber + ") < 0L");
        }

        if (frameNumber != 0) {
            blockingStrategy = FIXED_BLOCKSIZE_STREAM;
            sampleNumber = 0L;
        }

        this.frameNumber = frameNumber;
    }


    @XmlTransient
    private Frame frame;


    @XmlElement(required = true)
    private int blockingStrategy;


    @XmlElement(required = true)
    private int blockSizeHint;


    @XmlAttribute
    private int blockSize;


    @XmlElement(required = true)
    private int sampleRateHint;


    @XmlAttribute
    private int sampleRate;


    @XmlElement(required = true)
    private int channelAssignment;


    @XmlElement(required = true)
    private int sampleSizeHint;


    @XmlAttribute
    private int sampleSize;


    @XmlElement(required = true)
    private long sampleNumber;


    @XmlElement(required = true)
    private int frameNumber;


}

